Thredded is a great Ruby on Rails engine to support discussion forums within a Rails application.
It's got loads of features, from basic messageboard functionality to moderation, to LaTeX mark-up.
But it's designed to host one set of messageboards within an application, presenting a few
problems for applications that want to present the thredded interface to several different 
tenants, independent of each other.  Here's what we did to use thredded to power messageboards
for each project within [FromThePage](https://github.com/benwbrum/fromthepage)

## Connect a messageboard group to your application model
In our application, we we wanted the messageboard experience to be scoped to a single project -- a model we call a `Collection`.  The easiest way to accomplish this was to connect the `Collection` model to a `Thredded::MessageboardGroup` record, since thredded already has controllers and views for showing multiple message boards within the context of a single `MessageboardGroup`.

First, add the foreign key to your application's model:

```ruby
class AddMessageboardGroupToCollection < ActiveRecord::Migration[6.0]
  def change
    add_reference :collections, :thredded_messageboard_group, null: true, foreign_key: true
  end
end
```

This also requires an association within your model
```ruby
  belongs_to :messageboard_group, class_name: 'Thredded::MessageboardGroup', foreign_key: 'thredded_messageboard_group_id', optional: true
```

### Create default message boards and associate with your model
Since we wanted message boards to be an optional feature of our application (to be toggled on or off by administrative users on the `Collection` level), we also needed to add a flag for enabling/disabling message boards:
```ruby
class AddMesssageboardsEnabledToCollection < ActiveRecord::Migration[6.0]
  def change
    add_column :collections, :messageboards_enabled, :boolean
  end
end
```

Now we're ready to create message board groups when users enable the message board functionality.  We wanted to create a couple of default message boards when this happens, as well.

```ruby
  def enable_messageboards
    if self.messageboard_group.nil?
      self.messageboard_group = Thredded::MessageboardGroup.create!(name: self.title)
      # now create the default messageboards
      Thredded::Messageboard.create!(name: 'General', description: 'General discussion', messageboard_group_id: self.messageboard_group.id)
      Thredded::Messageboard.create!(name: 'Help', messageboard_group_id: self.messageboard_group.id)
    end
    self.messageboards_enabled = true
    self.save!
  end

  def disable_messageboards
    self.messageboards_enabled=false
    self.save!
  end
```

Now we run into the first real problem using Thredded in a multi-tenant environment -- we'd like to create a "General" messageboard for each tenant, but there is a unique validation rule on messagboard names.  This requires an override.

First, modify your application initializer to load overrides if you have not already.

In `config/application.rb` add this code:
```ruby
  # load overrides for Thredded and other engines
  # config/application.rb
    overrides = "#{Rails.root}/app/overrides"
    Rails.autoloaders.main.ignore(overrides)

    config.to_prepare do
      Dir.glob("#{overrides}/**/*_override.rb").each do |override|
        load override
      end
    end
```

Next, add an override to the Thredded model by creating `app/overrides/models/thredded/messageboard_override.rb` with these contents:
```ruby
Thredded::Messageboard.class_eval do
  clear_validators!
  validates :name,
            length: { within: Thredded.messageboard_name_length_range },
            presence: true
  validates :topics_count, numericality: true
  validates :position, presence: true, on: :update
end
```

Now you should be able to hook into the UI by updating the controller for the tenant model (`app/controllers/collection_controller.rb`)
```ruby
  def enable_messageboards
    @collection.enable_messageboards
    redirect_to edit_collection_path(@collection.owner, @collection)
  end

  def disable_messageboards
    @collection.disable_messageboards
    redirect_to edit_collection_path(@collection.owner, @collection)
  end
```

and modifying your view (`app/views/collection/edit.html.slim`)
```
    h3 =t('.message_boards')
    p =t('.message_boards_description')
    -if @collection.messageboards_enabled?
      =link_to(collection_disable_messageboards_path(:collection_id => @collection.id), class: 'button')
        span =t('.disable_message_boards')
    -else
      =link_to(collection_enable_messageboards_path(:collection_id => @collection.id), class: 'button')
        span =t('.enable_message_boards')
```

## Mounting Thredded under a tenant object
For the rest of our application, resources are mounted under a path containing the user slug and the collection slug.  We'd like thredded to appear under that, so that `/john-smith/first-project/forum` loads Thredded within the navigation framework used by other resources under `/john-smith/first-project`.

First, mount Thredded under the resource in `config/routes.rb`
```ruby
  scope ':user_slug' do
    scope ':collection_id' do
      mount Thredded::Engine => '/forum'
    end
  end
```

Next, add a reference to Thredded within your application navigation. (`/app/views/shared/_collection_tabs.html.slim`)
```
-if @collection.messageboards_enabled? && signed_in?
  -tabs +=[{\
  :name => t('.forum'),
  :selected => 14,
  :path => "#{Thredded::UrlsHelper::show_messageboard_group_path(@collection.messageboard_group, user_slug: @collection.owner.slug, collection_id: @collection.slug)}",
}]
```
(NB: If this is a partial rendered by your application and also by a Thredded layout, you'll need to prefix all your path helpers with `main_app.`)

Because our application only uses one layout to render all pages (navigation links like tabs are rendered by the views themselves, which is _not recommended_), we needed to render the tenant-specific navigation within a thredded-specific part of our application layout (`/app/layouts/application.html/slim`):
```
      -if content_for?(:thredded)
        =render({ :partial => '/shared/collection_tabs', :locals => { :selected => 14, :collection_id => @collection.id }})
```

But wait!  If Thredded will be rendering URLs using the user slug and collection slug, it needs to know what those values are!  We load a `@collection` object in `/app/controllers/application_controller.rb`, so after loading that we need to add this code to make the path helpers work:
```ruby
if self.class.module_parent == Thredded && @collection
  Thredded::Engine.routes.default_url_options = { user_slug: @collection.owner.slug, collection_id: @collection.slug }
else
  Thredded::Engine.routes.default_url_options = { user_slug: 'nil', collection_id: 'nil' }
end
```

## Suppressing cross-talk
Now we have a tab under our tenant object that renders thredded, with links back to the rest of our application.  All we need to do is suppress cross-talk, to keep users from seeing messageboards outside the context of their tenant object.  For this, we'll need to override a handful of Thredded views and controllers.

Suppress display of "Create Messageboard Group" and navigation to "All Messageboards" within these files:
* `app/views/thredded/messageboard_groups/show.html.erb`
* `app/views/thredded/messageboards/_form.html.erb`
* `app/views/thredded/shared/_breadcrumbs.html.erb`
* `app/views/thredded/topics/search.html.erb`

You'll also need to override a handful of actions on the Thredded controllers.

In `app/overrides/controllers/thredded/messageboards_controller_override.rb`
```ruby
Thredded::MessageboardsController.class_eval do

  def create
    @new_messageboard = Thredded::Messageboard.new(messageboard_params)
    authorize_creating @new_messageboard
    if Thredded::CreateMessageboard.new(@new_messageboard, thredded_current_user).run
      redirect_to Thredded::UrlsHelper::show_messageboard_group_path(@collection.messageboard_group)
    else
      render :new
    end
  end
end
```

And suppress the search across messageboard groups within the topic controller
In `app/overrides/controllers/thredded/topics_controller_override.rb`
```ruby
Thredded::TopicsController.class_eval do

  def search
    @query = params[:q].to_s
    # add messageboard_group_id
    messageboard_ids = @collection.messageboard_group.messageboards.map{|mb| mb.id}

    page_scope = topics_scope
      .where(messageboard_id: messageboard_ids)
      .search_query(@query)
      .order_recently_posted_first
      .includes(:categories, :last_user, :user)
      .send(Kaminari.config.page_method_name, current_page)
    return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
    @topics = Thredded::TopicsPageView.new(thredded_current_user, page_scope)
  end

end
```

The full list of changes we made to our application to integrate Thredded and make it multi-tenant are at [this pull request](https://github.com/benwbrum/fromthepage/pull/3345/files)

Many thanks to the folks running Thredded.
